11장: 재수행 가능 작업 (Rerunnable Jobs)

11장에서 다루는 내용

  • 재수행 가능 작업이란
  • 재수행 가능 작업과 장기 실행 작업의 차이
  • 재수행 가능 작업을 리소스로 표현하는 방법
  • LRO 반환 사용자 메서드를 통해 재수행 가능 작업을 구현하는 방식의 이점
  • 작업 실행 리소스를 통해 결과를 표현하는 방법

11.1 서론 — 왜 재수행 가능 작업이 필요한가?

핵심 키워드

용어 설명
재수행 가능 작업 (Rerunnable Job) 같은 설정으로 반복 실행할 수 있는 작업. "레시피"처럼 한번 만들어두면 여러 번 요리 가능
주문형 메서드 (On-demand Method) 필요할 때마다 직접 호출하는 방식. 매번 모든 설정을 전달해야 함
잡 (Job) 작업의 설정을 저장하는 리소스. 설정과 실행을 분리하는 핵심 개념

10장(LRO)만으로는 부족한 3가지 상황

10장에서 배운 LRO는 비동기 작업을 추적하는 훌륭한 패턴이다. 하지만 반복 실행이 필요한 경우 LRO만으로는 해결이 어렵다.

비유: 커피 주문

LRO (10장) = 카페에서 매번 주문하기
  "아이스 아메리카노, 샷 2, 시럽 1, 얼음 적게, 텀블러에..."
  → 매번 이 모든 설정을 말해야 함 😩

재수행 가능 작업 (11장) = 단골 주문 등록
  처음: "이 설정으로 '나의 커피'를 등록해주세요"
  이후: "나의 커피 한 잔 주세요" (설정 불필요!)
  → 심지어 "매일 오전 9시에 자동으로" 스케줄링도 가능 ☕

상황 1: 설정 매개변수가 너무 많다

주문형 메서드 호출 시:

  BackupChatRoom({
      id: "chatRooms/1",
      destination: "s3://my-bucket/backup",
      compressionFormat: "bz2",
      encryptionKey: "ak-12345-secret-key"
  });

  ⚠️ 매번 이 모든 값을 전달해야 한다!
  ⚠️ 홍길동의 노트북에 저장된 매개변수에 문제가 있으면?
  ⚠️ 소스 컨트롤이 매우 중요해짐

상황 2: 설정 권한과 실행 권한을 분리하고 싶다

개발팀 (설정 담당)              운영팀 (실행 담당)
─────────────────────        ─────────────────────
✅ 잡 생성/수정 가능             ✅ 잡 실행 가능
✅ 암호화 키 설정                ❌ 잡 설정 변경 불가
✅ 백업 위치 결정                ❌ 암호화 키 볼 수 없음
❌ 잡 직접 실행 안 함            ✅ "백업 실행" 버튼만 클릭

→ 주문형 메서드로는 이 분리가 어렵다!
  (실행하려면 모든 설정값을 알아야 하니까)

상황 3: 서버 측 스케줄링이 필요하다

클라이언트 스케줄링 (crontab)     서버 스케줄링 (Job + API)
──────────────────────────      ──────────────────────────
클라이언트가 항상 켜져 있어야 함    서버가 자동으로 실행
cron 서비스에 불완전한 의존        API 자체적으로 스케줄 관리
설정값이 crontab 스크립트에 분산   설정값이 Job 리소스에 집중

11.2 개괄 — Job 리소스의 기본 개념

핵심 키워드

용어 설명
Job 리소스 작업의 설정을 저장하는 API 리소스. 실행은 별도의 run 메서드로 수행
run 사용자 메서드 Job 리소스에 저장된 설정으로 실제 작업을 시작하는 메서드
설정과 실행의 분리 Job 패턴의 핵심 원칙. 설정(create)과 실행(run)을 두 단계로 나눔

Job 패턴의 핵심: 작업을 2단계로 나눈다

┌───────────────────────────────────────────────────────┐
│              주문형 메서드 (기존 방식)                    │
│                                                       │
│  사용자 ──[설정+실행]──> API ──> 백엔드                  │
│                                                       │
│  ⚠️ 한 번에 모든 것을 처리                              │
│                                                       │
├───────────────────────────────────────────────────────┤
│              Job 패턴 (재수행 가능 작업)                  │
│                                                       │
│  1단계: 설정                                            │
│  사용자 ──[CreateJob]──> API ──> Job 리소스 저장          │
│                                                       │
│  2단계: 실행 (언제든, 여러 번 가능!)                      │
│  사용자 ──[RunJob]──> API ──> LRO 생성 ──> 백엔드 실행   │
│                                                       │
└───────────────────────────────────────────────────────┘

Job 리소스 상호작용 흐름 (시퀀스 다이어그램)

사용자          프론트엔드         잡 저장소        작업 저장소       백엔드
  │                │                │                │              │
  │ ┌────────── Job 리소스 생성 ──────────┐          │              │
  │ │              │                │     │          │              │
  │ │ CreateJob()  │                │     │          │              │
  │ │─────────────>│ CreateJob()    │     │          │              │
  │ │              │───────────────>│     │          │              │
  │ │              │   Job{...}     │     │          │              │
  │ │              │<───────────────│     │          │              │
  │ │ Job{...}     │                │     │          │              │
  │ │<─────────────│                │     │          │              │
  │ └──────────────────────────────────────┘          │              │
  │                │                │                │              │
  │     (시간이 흐름... 설정 변경 가능)                  │              │
  │                │                │                │              │
  │ ┌────────── 잡 실행 ────────────────────┐         │              │
  │ │              │                │      │         │              │
  │ │ RunJob()     │                │      │         │              │
  │ │─────────────>│  RunJob()      │      │         │              │
  │ │              │───────────────>│      │         │              │
  │ │              │                │ CreateOperation()              │
  │ │              │                │───────────────>│              │
  │ │              │                │  Operation{..} │ BackupData() │
  │ │              │                │<───────────────│─────────────>│
  │ │              │  Operation{..} │                │ 202 Accepted │
  │ │              │<───────────────│                │<─────────────│
  │ │ Operation{..}│                │                │              │
  │ │<─────────────│                │                │              │
  │ └──────────────────────────────────────┘         │              │
  │                │                │                │              │
  │  이후 과정은 표준 LRO 시퀀스를 따름                   │              │

Job이 해결하는 3가지 문제

┌─────────────┬─────────────────────────────────────────────┐
│ 문제          │ Job이 해결하는 방법                           │
├─────────────┼─────────────────────────────────────────────┤
│ 매개변수 과다 │ Job 생성 시 한 번만 설정, 이후 run은           │
│              │ 매개변수 없이 ID만 전달                        │
├─────────────┼─────────────────────────────────────────────┤
│ 권한 분리    │ Job CRUD와 run을 별개 메서드로 분리            │
│              │ → 개발팀은 설정, 운영팀은 실행만 가능           │
├─────────────┼─────────────────────────────────────────────┤
│ 스케줄링     │ run 메서드를 서버가 스케줄에 맞춰 자동 호출     │
│              │ → 클라이언트 없이도 주기적 실행 가능            │
└─────────────┴─────────────────────────────────────────────┘

11.3 구현

재수행 가능 작업을 구현하려면 2가지 핵심 구성 요소가 필요하다: 1. Job 리소스 정의 (및 표준 CRUD 메서드) 2. run 사용자 메서드 정의 (실제 작업 실행)


11.3.1 잡 리소스 — 설정을 저장하는 그릇

핵심 키워드

용어 설명
BackupChatRoomJob 채팅방 백업 설정을 저장하는 Job 리소스 예시
불변성 (Immutability) 한번 만든 리소스를 수정하지 않는 특성. 동시성 문제 방지
표준 메서드 컬렉션 Get, Create, Update, Delete, List — Job 리소스를 관리하는 CRUD 메서드

주문형 메서드 (기존 방식)

// 리스트 11.1 — 주문형 backup 사용자 메서드

abstract class ChatRoomApi {
    @post("/{id=chatRooms/*}:backup")
    BackupChatRoom(req: BackupChatRoomRequest):
        Operation<BackupChatRoomResponse, BackupChatRoomMetadata>;
}

interface BackupChatRoomRequest {
    id: string;               // 백업할 ChatRoom
    destination: string;      // 저장 위치 (예: s3://bucket/path)
    compressionFormat: string; // 압축 방식 (예: "bz2")
    encryptionKey: string;    // 암호화 키
}

interface BackupChatRoomResponse {
    destination: string;      // 백업 완료된 위치
}

interface BackupChatRoomMetadata {
    messagesCounted: number;
    messagesProcessed: number;
    bytesWritten: number;
}

Job 리소스로 전환

// 리스트 11.2 — 채팅 데이터 백업을 위한 잡 리소스

interface BackupChatRoomJob {
    id: string;               // 고유 식별자 (API 서버가 자동 생성)
    chatRoom: string;         // 백업할 ChatRoom 참조
    destination: string;      // 저장 위치
    compressionFormat: string; // 압축 방식
}
// ↑ 요청 메시지의 모든 필드가 Job 리소스 안에 저장됨!
비유: 주문형 vs Job

주문형 = 매번 처음부터 주문서 작성
  "1번 채팅방을 s3://bucket에 bz2로 압축하고 ak-12345로 암호화해서 백업해줘"

Job = 주문서를 미리 만들어 놓기
  처음: "이 주문서를 '일일백업'이라고 저장해줘"
  이후: "'일일백업' 실행해줘"  ← 이것만 하면 됨!

Job의 표준 메서드 (CRUD)

┌──────────────────────────┬─────────────────────────────┐
│ 표준 메서드                │ 역할                         │
├──────────────────────────┼─────────────────────────────┤
│ GetBackupChatRoomJob     │ 백업 작업의 세부 설정 표시     │
│ CreateBackupChatRoomJob  │ 새로운 재수행 가능 백업 작업 생성│
│ UpdateBackupChatRoomJob  │ 기존 재수행 가능 백업 작업 수정 │
│ DeleteBackupChatRoomJob  │ 기존 재수행 가능 백업 작업 제거 │
│ ListBackupChatRoomJobs   │ 재수행 가능한 백업 작업 목록 조회│
└──────────────────────────┴─────────────────────────────┘

불변성(Immutability) 선택

수정 가능한 Job (Update 허용):
  ✅ 설정 변경이 유연
  ⚠️ 잡을 실행하는 동안 업데이트하면? → 동시성 문제!

불변 Job (Update 차단):
  ✅ 동시성 문제 없음
  ✅ 설정이 바뀌지 않아 예측 가능
  ❌ 변경하려면 기존 삭제 → 새로 생성

→ 잡 실행 중 업데이트가 걱정되면 Update 메서드를 막는 것도 방법

11.3.2 run 사용자 메서드 — 잡을 실행하기

핵심 키워드

용어 설명
RunJob Job 리소스에 저장된 설정으로 실제 작업을 시작하는 사용자 메서드
매개변수 없음 run 메서드는 Job ID만 받음. 모든 설정은 이미 Job에 저장됨
LRO 반환 run 메서드도 비동기 작업이므로 Operation을 반환
// 리스트 11.3 — 재수행 백업 잡을 위한 run 사용자 메서드

abstract class ChatRoomApi {
    @post("/{id=backupChatRoomJobs/*}:run")
    RunBackupChatRoomJob(req: RunBackupChatRoomJobRequest):
        Operation<RunBackupChatRoomJobResponse,
                  RunBackupChatRoomJobMetadata>;
}

interface RunBackupChatRoomJobRequest {
    id: string;     // ← 실행할 Job의 ID만 전달! 추가 설정 없음!
}

interface RunBackupChatRoomJobResponse {
    destination: string;    // 백업된 위치
}

interface RunBackupChatRoomJobMetadata {
    messagesCounted: number;
    messagesProcessed: number;
    bytesWritten: number;
}
비유: 세탁기 사용

Job 생성 = 세탁 프로그램 설정
  "울 코스, 30도, 약탈수, 1200rpm"
  → 리모컨에 '코스 1'로 저장

run 실행 = 시작 버튼 누르기
  '코스 1' 실행!
  → 매번 설정할 필요 없이 버튼만 누름
  → 다음 주에도, 다다음 주에도 같은 코스 실행 가능

run의 핵심 규칙

1. run 메서드의 입력값은 Job 리소스의 ID뿐이다
   → 실행 시점에 추가 설정을 제공하지 않음

2. run 메서드는 LRO(Operation)를 반환한다
   → 10장에서 배운 폴링/대기 패턴 그대로 적용

3. 응답/메타데이터 인터페이스 이름이 바뀐다
   → BackupChatRoomResponse → RunBackupChatRoomJobResponse
   → 내용은 동일하지만 Job 맥락에 맞게 이름 변경

4. 같은 Job을 여러 번 실행할 수 있다
   → 그래서 "재수행 가능(Rerunnable)" 작업!

11.3.3 잡 실행 리소스 (Execution) — 실행 결과를 영구 저장

핵심 키워드

용어 설명
Execution (익스큐션) 잡의 각 실행 결과를 영구 저장하는 리소스. Job의 서브리소스
영속성 문제 LRO는 만료되어 삭제되지만, 실행 결과는 영구 보관이 필요할 수 있음
읽기 전용 리소스 Execution은 내부적으로만 생성됨. 사용자가 직접 생성/수정 불가

왜 Execution 리소스가 필요한가?

문제: LRO는 만료된다!

  잡 실행 → Operation 생성 → 30일 후 자동 삭제!
  "지난달 분석 결과 보여줘" → 404 Not Found 😱

  하지만 어떤 잡의 결과는 영구적으로 보관해야 한다!

해결: Execution 리소스로 결과를 별도 저장

  잡 실행 → Operation 생성 (임시)
         → Execution 리소스 생성 (영구!)
  "지난달 분석 결과 보여줘" → Execution에서 조회 ✅

어떤 잡에 Execution이 필요한가?

Execution이 필요 없는 경우:
  - 백업 잡 → 결과가 외부 스토리지(S3)에 저장됨
  - 리소스 생성 잡 → 결과가 새 API 리소스로 존재
  → LRO 결과 타입으로 충분

Execution이 필요한 경우:
  - 분석 잡 → 결과(감정 점수, 복잡도 등)를 API에 영구 보관해야 함
  - 기존 리소스를 만들지도, 외부에 저장하지도 않는 작업
  → Execution 리소스로 결과를 저장

Execution 리소스 예시

// 리스트 11.4 — 채팅 데이터 분석을 위한 Execution 리소스

interface AnalyzeChatRoomJobExecution {
    id: string;                    // 고유 식별자
    job: AnalyzeChatRoomJob;       // 실행에 사용된 Job의 스냅샷
    sentenceComplexity: number;    // 문장 복잡도 분석 결과
    sentiment: number;             // 감정 분석 결과
    abuseScore: number;            // 부적절 문구 점수
}
비유: 시험 결과표

  Job (시험 설정) = "수학 중간고사, 50문제, 시간 90분"
  Execution (시험 결과) = "홍길동, 85점, 2024-03-10 응시"

  같은 시험(Job)을 여러 번 치르면(run),
  각각의 성적표(Execution)가 생긴다!

  analyzeChatRoomJobs/job-1/executions/exec-1  ← 1회차 결과
  analyzeChatRoomJobs/job-1/executions/exec-2  ← 2회차 결과
  analyzeChatRoomJobs/job-1/executions/exec-3  ← 3회차 결과

Execution의 run 사용자 메서드

// 리스트 11.5 — Execution 리소스를 다루는 run 사용자 메서드 정의

abstract class ChatRoomApi {
    // Job 생성
    @post("/analyzeChatRoomJobs")
    CreateAnalyzeChatRoomJob(req: CreateAnalyzeChatRoomJobRequest):
        AnalyzeChatRoomJob;

    // Job 실행 → Execution 리소스를 결과로 반환!
    @post("/{id=analyzeChatRoomJobs/*}:run")
    RunAnalyzeChatRoomJob(req: RunAnalyzeChatRoomJobRequest):
        Operation<AnalyzeChatRoomJobExecution,  // ← 결과가 Execution!
                  RunAnalyzeChatRoomJobMetadata>;
}

Execution 리소스와 LRO의 관계

                    RunJob() 호출
                         │
                         ▼
              ┌─────────────────────┐
              │    Operation 생성     │ ← LRO (임시, 만료됨)
              │    done: false       │
              └──────────┬──────────┘
                         │
                    작업 진행 중...
                         │
                         ▼
              ┌─────────────────────┐
              │   백엔드 작업 완료     │
              └──────────┬──────────┘
                         │
                   ┌─────┴──────┐
                   ▼            ▼
         ┌──────────────┐  ┌────────────────────┐
         │ Operation    │  │ Execution 리소스     │ ← 영구 보관!
         │ done: true   │  │ 분석 결과 저장       │
         │ result:      │  │ id: "exec-1"       │
         │  Execution ──┼──│ sentiment: 0.82    │
         │  참조         │  │ abuseScore: 0.03   │
         └──────────────┘  └────────────────────┘
              30일 후 삭제      영구 보관!

Execution 리소스의 표준 메서드

┌────────────────────────────────────┬──────────────────────┐
│ 표준 메서드                          │ 역할                  │
├────────────────────────────────────┼──────────────────────┤
│ GetAnalyzeChatRoomJobExecution     │ 특정 실행 결과 조회     │
│ ListAnalyzeChatRoomJobExecutions   │ 실행 결과 목록 조회     │
│ DeleteAnalyzeChatRoomJobExecution  │ (선택) 특정 실행 삭제   │
├────────────────────────────────────┼──────────────────────┤
│ ❌ Create                          │ 직접 생성 불가!        │
│ ❌ Update                          │ 수정 불가 (읽기 전용)  │
└────────────────────────────────────┴──────────────────────┘

Execution은 run 메서드에 의해 내부적으로만 생성됨
→ 사용자가 명시적으로 생성하거나 수정할 수 없다!

Execution 리소스의 계층 구조

Execution은 Job의 자식(서브리소스)으로 배치하는 것이 적합

/analyzeChatRoomJobs/job-1                    ← Job 리소스
/analyzeChatRoomJobs/job-1/executions/exec-1  ← Execution 1
/analyzeChatRoomJobs/job-1/executions/exec-2  ← Execution 2

이유:
- "이 특정 잡에 대한 실행 결과"라는 질문에 자연스럽게 대응
- 잡의 맥락 안에서 실행 범위를 정하는 것이 적합
- LRO의 최상위 /operations 컬렉션과 달리,
  Execution은 특정 잡 타입 내에서만 동작

11.3.4 최종 API 정의

// 리스트 11.6 — 채팅방 메시지를 분석하기 위한 최종 API 정의

abstract class ChatRoomApi {

    // ═══════════════════════════════════════
    // Job 리소스 표준 메서드
    // ═══════════════════════════════════════

    @post("/analyzeChatRoomJobs")
    CreateAnalyzeChatRoomJob(req: CreateAnalyzeChatRoomJobRequest):
        AnalyzeChatRoomJob;

    @get("/analyzeChatRoomJobs")
    ListAnalyzeChatRoomJobs(req: ListAnalyzeChatRoomJobsRequest):
        ListAnalyzeChatRoomJobsResponse;

    @get("/{id=analyzeChatRoomJobs/*}")
    GetAnalyzeChatRoomJob(req: GetAnalyzeChatRoomJobRequest):
        AnalyzeChatRoomJob;

    @patch("/{resource.id=analyzeChatRoomJobs/*}")
    UpdateAnalyzeChatRoomJob(req: UpdateAnalyzeChatRoomJobRequest):
        AnalyzeChatRoomJob;

    // ═══════════════════════════════════════
    // run 사용자 메서드 (잡 실행)
    // ═══════════════════════════════════════

    @post("/{id=analyzeChatRoomJobs/*}:run")
    RunAnalyzeChatRoomJob(req: RunAnalyzeChatRoomJobRequest):
        Operation<AnalyzeChatRoomJobExecution,
                  RunAnalyzeChatRoomJobMetadata>;

    // ═══════════════════════════════════════
    // Execution 리소스 표준 메서드
    // ═══════════════════════════════════════

    @get("/{parent=analyzeChatRoomJobs/*}/executions")
    ListAnalyzeChatRoomJobExecutions(
        req: ListAnalyzeChatRoomJobExecutionsRequest):
        ListAnalyzeChatRoomJobExecutionsResponse;

    @get("/{id=analyzeChatRoomJobs/*/executions/*}")
    GetAnalyzeChatRoomJobExecution(
        req: GetAnalyzeChatRoomJobExecutionRequest):
        AnalyzeChatRoomJobExecution;
}

// ═══════════════════════════════════════
// Job 리소스 인터페이스
// ═══════════════════════════════════════

interface AnalyzeChatRoomJob {
    id: string;
    chatRoom: string;
    destination: string;
    compressionFormat: string;
}

// ═══════════════════════════════════════
// Execution 리소스 인터페이스
// ═══════════════════════════════════════

interface AnalyzeChatRoomJobExecution {
    id: string;
    job: AnalyzeChatRoomJob;       // 실행 시점의 Job 스냅샷
    sentenceComplexity: number;
    sentiment: number;
    abuseScore: number;
}

// ═══════════════════════════════════════
// 요청/응답 인터페이스
// ═══════════════════════════════════════

interface CreateAnalyzeChatRoomJobRequest {
    resource: AnalyzeChatRoomJob;
}

interface ListAnalyzeChatRoomJobsRequest {
    filter: string;
}

interface ListAnalyzeChatRoomJobsResponse {
    results: AnalyzeChatRoomJob[];
}

interface GetAnalyzeChatRoomJobRequest {
    id: string;
}

interface UpdateAnalyzeChatRoomJobRequest {
    resource: AnalyzeChatRoomJob;
    fieldMask: string;
}

interface RunAnalyzeChatRoomJobRequest {
    id: string;       // Job ID만 전달!
}

interface RunAnalyzeChatRoomJobMetadata {
    messagesProcessed: number;
    messagesCounted: number;
}

interface ListAnalyzeChatRoomJobExecutionsRequest {
    parent: string;   // 예: "analyzeChatRoomJobs/job-1"
    filter: string;
}

interface ListAnalyzeChatRoomJobExecutionsResponse {
    results: AnalyzeChatRoomJobExecution[];
}

interface GetAnalyzeChatRoomJobExecutionRequest {
    id: string;
}

11.4 트레이드오프

✅ 재수행 가능 작업의 장점
─────────────────────────────────────────
- 설정과 실행을 분리하여 관리가 용이
- 권한을 세분화하여 보안 강화
- 서버 측 스케줄링 가능
- 동일한 설정으로 반복 실행 가능

⚠️ 고려할 점
─────────────────────────────────────────
1. 재수행 가능 작업만이 정답은 아니다
   → 간단한 일회성 작업은 주문형 메서드가 더 적합

2. 권한을 더 세밀하게 나눌 수도 있다
   → 특정 API 메서드의 매개변수 세트까지 제한하는
     고급 권한 시스템도 가능 (다만 구현이 복잡)

3. Execution 리소스를 사용하면 결과를 영구 보관 가능
   → Operation 리소스 목록을 필터링하는 것보다 깔끔
   → 단점: Job과 연관된 리소스만 따로 얻어야 함

4. 사용자 입장에서는 번거로울 수 있다
   → 메서드를 두 부분(설정+실행)으로 나누는 것이
     더 세분화된 방법이기도 하다

11.5 연습문제

문제 1: 재수행 가능 작업에서 동작을 수행하거나 이를 설정하기 위한 권한을 어떻게 구별하는가?

: - Job 리소스의 CRUD 메서드(Create, Update, Delete)에 대한 권한 = 설정 권한 - run 사용자 메서드에 대한 권한 = 실행 권한 - 두 메서드가 분리되어 있으므로 각각에 접근할 수 있는 주체를 따로 제어 가능 - 예: 개발팀은 Job 생성/수정만, 운영팀은 Job 실행만 허용

문제 2: 잡을 실행하는 과정에서 새로운 리소스가 생성된다면 그 결과는 익스큐션인가, 아니면 생성된 리소스인가?

: - 생성된 리소스가 결과다. - 예: ImportChatRoomsJob을 실행하면 새 ChatRoom 리소스가 생성됨 - 이 경우 Operation의 ResultT는 새로 생성된 리소스 타입이 됨 - Execution 리소스는 기존 리소스를 만들지도, 외부에 저장하지도 않는 경우에만 사용 - 즉, 잡의 결과가 API 내부에 별도로 존재하지 않을 때 Execution으로 영구 저장

문제 3: 익스큐션 리소스를 명시적으로 생성하면 안 되는 이유는 무엇인가?

: - Execution은 run 메서드의 내부 처리 결과로만 생성되어야 함 - 사용자가 직접 Create하면 실제로 작업을 실행하지 않고도 가짜 결과를 만들 수 있음 - Execution은 불변성을 가져야 하므로 Update도 구현하지 않음 - 읽기 전용 리소스(Get, List)와 선택적 Delete만 제공 - create 표준 메서드가 LRO 작업 완료 시에 리소스를 자동 생성하는 패턴과 유사


요약

  • 재수행 가능 작업은 사용자가 작업을 설정하고, 그 작업을 실행하는 과정을 분리하는 좋은 방법이다.
  • 잡 리소스는 우선 생성된 다음에 설정이 이뤄진다. 이 잡들은 나중에 run 사용자 리소스를 통해 실행된다.
  • 잡이 기존 리소스에서 작동하거나 이를 생성하지 않는다면 일반적으로 이 Job 리소스의 결과는 Execution 리소스 형태로 나타난다.
  • Execution 리소스는 읽기 전용 리소스와 유사하기에 목록을 출력하거나 데이터를 취득할 수는 있지만 명시적으로 업데이트하거나 생성될 수는 없다.

부록 A: Flask/Python으로 구현하는 재수행 가능 작업

"""
재수행 가능 작업(Rerunnable Job) 패턴을 Flask로 구현한 예제.
채팅방 메시지 분석 잡을 설정하고 실행하는 API 서버.
"""
from flask import Flask, jsonify, request
import uuid
import time
import threading
import random
from datetime import datetime, timedelta

app = Flask(__name__)

# 저장소 (실제로는 DB 사용)
jobs = {}          # Job 리소스
executions = {}    # Execution 리소스
operations = {}    # LRO


# ═══════════════════════════════════════
# 헬퍼 함수
# ═══════════════════════════════════════

def create_operation(result_type_hint, metadata=None):
    """새 Operation 리소스를 생성한다."""
    op_id = f"operations/{uuid.uuid4().hex[:8]}"
    operations[op_id] = {
        "id": op_id,
        "done": False,
        "expireTime": (datetime.now() + timedelta(days=30)).isoformat(),
        "result": None,
        "metadata": metadata or {}
    }
    return operations[op_id]


# ═══════════════════════════════════════
# 백그라운드 분석 작업
# ═══════════════════════════════════════

def analyze_background(op_id, job_data):
    """
    백그라운드에서 채팅방 분석을 수행한다.
    완료 시 Execution 리소스를 생성한다.
    """
    total = 200
    for i in range(0, total + 1, 40):
        time.sleep(1)
        if op_id not in operations:
            return
        operations[op_id]["metadata"]["messagesProcessed"] = i
        operations[op_id]["metadata"]["messagesCounted"] = total

    # Execution 리소스 생성 (영구 보관)
    exec_id = f"{job_data['id']}/executions/{uuid.uuid4().hex[:6]}"
    execution = {
        "id": exec_id,
        "job": job_data.copy(),    # Job 스냅샷 저장
        "sentenceComplexity": round(random.uniform(0.3, 0.9), 2),
        "sentiment": round(random.uniform(-1.0, 1.0), 2),
        "abuseScore": round(random.uniform(0.0, 0.5), 2),
        "createdAt": datetime.now().isoformat()
    }
    executions[exec_id] = execution

    # Operation 완료 처리 (결과 = Execution 참조)
    operations[op_id]["done"] = True
    operations[op_id]["result"] = execution


# ═══════════════════════════════════════
# Job 표준 메서드 (CRUD)
# ═══════════════════════════════════════

@app.route("/analyzeChatRoomJobs", methods=["POST"])
def create_job():
    """새 분석 잡을 생성한다."""
    data = request.json or {}
    job_id = f"analyzeChatRoomJobs/{uuid.uuid4().hex[:8]}"
    job = {
        "id": job_id,
        "chatRoom": data.get("chatRoom", "chatRooms/1"),
        "destination": data.get("destination", ""),
        "compressionFormat": data.get("compressionFormat", "none"),
    }
    jobs[job_id] = job
    return jsonify(job), 201


@app.route("/analyzeChatRoomJobs", methods=["GET"])
def list_jobs():
    """모든 분석 잡을 목록으로 조회한다."""
    return jsonify({"results": list(jobs.values())})


@app.route("/analyzeChatRoomJobs/<job_id>", methods=["GET"])
def get_job(job_id):
    """특정 분석 잡의 세부 설정을 조회한다."""
    full_id = f"analyzeChatRoomJobs/{job_id}"
    job = jobs.get(full_id)
    if not job:
        return jsonify({"code": "NOT_FOUND", "message": "잡을 찾을 수 없습니다"}), 404
    return jsonify(job)


@app.route("/analyzeChatRoomJobs/<job_id>", methods=["PATCH"])
def update_job(job_id):
    """기존 분석 잡의 설정을 수정한다."""
    full_id = f"analyzeChatRoomJobs/{job_id}"
    job = jobs.get(full_id)
    if not job:
        return jsonify({"code": "NOT_FOUND", "message": "잡을 찾을 수 없습니다"}), 404

    data = request.json or {}
    for key in ["chatRoom", "destination", "compressionFormat"]:
        if key in data:
            job[key] = data[key]
    return jsonify(job)


@app.route("/analyzeChatRoomJobs/<job_id>", methods=["DELETE"])
def delete_job(job_id):
    """기존 분석 잡을 삭제한다."""
    full_id = f"analyzeChatRoomJobs/{job_id}"
    if full_id not in jobs:
        return jsonify({"code": "NOT_FOUND", "message": "잡을 찾을 수 없습니다"}), 404
    del jobs[full_id]
    return "", 204


# ═══════════════════════════════════════
# run 사용자 메서드 (잡 실행)
# ═══════════════════════════════════════

@app.route("/analyzeChatRoomJobs/<job_id>:run", methods=["POST"])
def run_job(job_id):
    """잡을 실행한다. LRO(Operation)를 반환한다."""
    full_id = f"analyzeChatRoomJobs/{job_id}"
    job = jobs.get(full_id)
    if not job:
        return jsonify({"code": "NOT_FOUND", "message": "잡을 찾을 수 없습니다"}), 404

    # Operation 생성 (LRO)
    op = create_operation(
        result_type_hint="AnalyzeChatRoomJobExecution",
        metadata={"messagesProcessed": 0, "messagesCounted": 0}
    )

    # 백그라운드에서 분석 실행
    thread = threading.Thread(
        target=analyze_background,
        args=(op["id"], job)
    )
    thread.daemon = True
    thread.start()

    return jsonify(op), 202


# ═══════════════════════════════════════
# Execution 표준 메서드 (읽기 전용)
# ═══════════════════════════════════════

@app.route("/analyzeChatRoomJobs/<job_id>/executions", methods=["GET"])
def list_executions(job_id):
    """특정 잡의 모든 실행 결과를 목록으로 조회한다."""
    prefix = f"analyzeChatRoomJobs/{job_id}/executions/"
    results = [e for eid, e in executions.items() if eid.startswith(prefix)]
    return jsonify({"results": results})


@app.route(
    "/analyzeChatRoomJobs/<job_id>/executions/<exec_id>",
    methods=["GET"]
)
def get_execution(job_id, exec_id):
    """특정 실행 결과를 조회한다."""
    full_id = f"analyzeChatRoomJobs/{job_id}/executions/{exec_id}"
    execution = executions.get(full_id)
    if not execution:
        return jsonify({"code": "NOT_FOUND", "message": "실행 결과를 찾을 수 없습니다"}), 404
    return jsonify(execution)


# ═══════════════════════════════════════
# Operation 조회 (10장에서 배운 것)
# ═══════════════════════════════════════

@app.route("/operations/<op_id>", methods=["GET"])
def get_operation(op_id):
    """Operation 상태를 조회한다."""
    full_id = f"operations/{op_id}"
    op = operations.get(full_id)
    if not op:
        return jsonify({"code": "NOT_FOUND", "message": "작업을 찾을 수 없습니다"}), 404
    return jsonify(op)


if __name__ == "__main__":
    app.run(port=5001, debug=True)

부록 B: 클라이언트 사용 예제

"""
재수행 가능 작업 API를 사용하는 클라이언트 예제.
1단계: 잡 생성 (설정)
2단계: 잡 실행 (여러 번 가능)
3단계: Execution 결과 조회
"""
import requests
import time

BASE = "http://localhost:5001"


def main():
    # ── 1단계: 분석 잡 생성 ──
    print("=== 1단계: 잡 생성 ===")
    resp = requests.post(f"{BASE}/analyzeChatRoomJobs", json={
        "chatRoom": "chatRooms/1",
        "destination": "s3://my-bucket/analysis",
        "compressionFormat": "gzip"
    })
    job = resp.json()
    job_id = job["id"].split("/")[1]
    print(f"  잡 생성 완료: {job['id']}")

    # ── 2단계: 잡 실행 (1회차) ──
    print("\n=== 2단계: 잡 실행 (1회차) ===")
    resp = requests.post(f"{BASE}/analyzeChatRoomJobs/{job_id}:run")
    operation = resp.json()
    op_id = operation["id"].split("/")[1]
    print(f"  Operation 생성: {operation['id']}")

    # 폴링으로 완료 대기
    while not operation["done"]:
        time.sleep(2)
        resp = requests.get(f"{BASE}/operations/{op_id}")
        operation = resp.json()
        meta = operation.get("metadata", {})
        print(f"  진행: {meta.get('messagesProcessed', 0)}"
              f"/{meta.get('messagesCounted', 0)}")

    print(f"  완료! 감정 점수: {operation['result']['sentiment']}")

    # ── 3단계: 같은 잡을 다시 실행 (2회차) ──
    print("\n=== 3단계: 잡 재실행 (2회차) ===")
    resp = requests.post(f"{BASE}/analyzeChatRoomJobs/{job_id}:run")
    operation2 = resp.json()
    op_id2 = operation2["id"].split("/")[1]

    while not operation2["done"]:
        time.sleep(2)
        resp = requests.get(f"{BASE}/operations/{op_id2}")
        operation2 = resp.json()

    print(f"  완료! 감정 점수: {operation2['result']['sentiment']}")

    # ── 4단계: Execution 목록 조회 ──
    print("\n=== 4단계: Execution 목록 조회 ===")
    resp = requests.get(f"{BASE}/analyzeChatRoomJobs/{job_id}/executions")
    execs = resp.json()["results"]
    for i, ex in enumerate(execs, 1):
        print(f"  {i}회차: 감정={ex['sentiment']}, "
              f"복잡도={ex['sentenceComplexity']}, "
              f"부적절={ex['abuseScore']}")


if __name__ == "__main__":
    main()

부록 C: 주문형 메서드 vs Job 패턴 전체 비교

┌──────────────────┬─────────────────────┬──────────────────────────┐
│                  │ 주문형 메서드          │ Job 패턴 (재수행 가능)     │
├──────────────────┼─────────────────────┼──────────────────────────┤
│ 호출 방식         │ 매번 모든 설정 전달   │ 설정(Job) 1회 + run N회   │
│ 설정 저장         │ 클라이언트 책임       │ 서버에 Job으로 저장        │
│ 반복 실행         │ 매번 동일 설정 전달   │ run만 호출 (ID만 전달)    │
│ 권한 분리         │ 어렵다               │ CRUD ↔ run 분리 가능     │
│ 스케줄링         │ 클라이언트(cron)      │ 서버 자체 스케줄링 가능    │
│ 결과 보관        │ LRO (만료됨)          │ Execution (영구!)         │
│ 적합한 경우      │ 일회성, 간단한 작업    │ 반복적, 설정 복잡한 작업   │
│ API 복잡도       │ 낮음                 │ 높음 (Job+run+Execution) │
└──────────────────┴─────────────────────┴──────────────────────────┘
난이도
에피소드
질문
카드를 로딩 중...
답변

클릭하거나 Space를 눌러 뒤집기

0 / 0
학습 진도 0%
이동   Space 뒤집기   R 셔플